/*
 * ============================================================================
 *                   GNU Lesser General Public License
 * ============================================================================
 *
 *
 *
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
 *
 *
 *
 */
package rad.framework.seam;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.ejb.Remove;
import javax.persistence.EntityManager;
import javax.persistence.Query;

import rad.framework.jpa.EntityPersistenceContextMap;
import rad.framework.jpa.HibernateUtil;
import rad.framework.jpa.QueryUtil;
import rad.framework.lang.ReflectionUtil;
import rad.framework.security.jpa.Action;
import rad.framework.security.jpa.PermissionsUtil;
import rad.framework.xml.BetwixtUtil;
import rad.framework.xml.BetwixtUtil.EmptySuppressionStrategy;

import org.apache.commons.lang.StringUtils;
import org.hibernate.Session;
import org.jboss.seam.Component;
import org.jboss.seam.Seam;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.core.ResourceBundle;
import org.jboss.seam.framework.EntityController;
import org.jboss.seam.international.StatusMessage.Severity;
import org.jboss.seam.persistence.PersistenceProvider;
import org.jboss.seam.security.Identity;
import org.jboss.seam.web.Parameters;

/**
 * The base implementation of the Finder interface.
 *
 * @author boudyacho
 */
public abstract class AbstractFinderBean<E> extends EntityController {

	// ------------------------------------
	// filter access and initiation
	// ------------------------------------

	private Filter filter = null;

	public Filter getFilter() {
		if (filter == null) {
			setFilter(new Filter());
			initFilter(filter);
		}
		return filter;
	}

	protected void initFilter(Filter filter) {
	}

	protected void setFilter(Filter filter) {
		this.filter = filter;
		if (filter != null) {
			this.filter.setEntityClass(getEntityClass());
		}
	}

	public String clearFilter() {
		getFilter().clear();
		return null;
	}

	public static final String FILTER_SELECTED_EVENT = "Filter.selected";
	
    @Observer(value = FILTER_SELECTED_EVENT, create = false)
    public void filterSelected(Filter filter) {
		updateResults();
		pager.setPageNumber(0);
		setFilter(filter);
    }

	public static final String FILTER_DELETED_EVENT = "Filter.deleted";
	
    @Observer(value = FILTER_DELETED_EVENT, create = false)
    public void filterDeleted() {
		updateResults();
		pager.setPageNumber(0);
		setFilter(null);
    }

	// ------------------------------------
	// query results related
	// ------------------------------------

	protected List<E> results = null;
	
    public void updateResults() {
		results = null;
    }

	protected void addUpdateListener() {
		getEvents().addListener(
				"org.jboss.seam.afterTransactionSuccess." + getEntityName(),
				"#{" + getName() + ".updateResults}");
	}
    
	public List<E> getResult() {
		if (results == null) { // DataModel?
			results = executeQuery(true);
		}
		return results;

	}

	public List<E> getReportResult() {
		// TODO need some sort of report paging such as a special list class
		// (see jasper reports jpa support)
		return executeQuery(false);
	}

	@SuppressWarnings("unchecked")
	protected List<E> executeQuery(boolean paging) {
		enableFilters(HibernateUtil.getSession(getEntityManager()));// TODO
		Map<String, Object> parameters = new HashMap<String, Object>();

		String eql = getQueryString(parameters); // getEql
		String ceql = QueryUtil.getCountEjbql(eql, getEntityName(), "t");

		debug("eql: #0", eql);
		debug("ceql: #0", ceql);
		debug("parameters: #0", parameters);

		Query cqry = createQuery(ceql);
		QueryUtil.setParameters(cqry, parameters);
		pager.setObjectCount((Long) cqry.getSingleResult());

		Query qry = createQuery(eql);
		QueryUtil.setParameters(qry, parameters);

		if (paging) {
			qry.setFirstResult(pager.getPageSize() * pager.getPageNumber());
			qry.setMaxResults(pager.getPageSize());
		}

		return qry.getResultList();
	}

	/**
	 * Override this method to enable entity specific filters.
	 */
	protected void enableFilters(Session session) {
		// http://www.next-presso.fr/2008/10/how-to-use-hibernate-filters-in-seam/		
	}

	protected String getQueryString(Map<String, Object> parameters) {
		StringBuffer buffer = new StringBuffer();
		buffer.append("select ");
		// TODO doesn't work when orderBy an association
		// if (getFilter().isDistinct()) {
		// buffer.append("distinct ");
		// }
		buffer.append("t from ");
		buffer.append(getEntityName());
		buffer.append(" t ");
		addOuterJoins(buffer);

		// TODO model after combo & picker?
		
		String filter = getFilterString(parameters);
		String permissions = getPermissionFilter();
		if (!StringUtils.isEmpty(filter) || !StringUtils.isEmpty(permissions)) {
			buffer.append(" where ");
		}
		if (!StringUtils.isEmpty(filter)) {
			buffer.append(filter);
		}
		if (!StringUtils.isEmpty(filter) && !StringUtils.isEmpty(permissions)) {
			buffer.append(" and ");
		}
		if (!StringUtils.isEmpty(permissions)) {
			buffer.append(permissions);
		}

		buffer.append(" order by t.");
		buffer.append(orderBy.toString());
		return buffer.toString();
	}

	protected String getFilterString(Map<String, Object> parameters) {
		Filter f = getFilter();
		return f.getFilterString(parameters);
	}

	protected String getPermissionFilter() {
		String permissions = null;
		Identity identity = Identity.instance();
		if (identity.isLoggedIn()) {
			permissions = PermissionsUtil.getFilter(identity.getSubject(),
					identity.getPrincipal(), getEntityClass(), "t");
		} else {
			permissions = PermissionsUtil.getFilter(getEntityClass(), "t");
		}
		return permissions;
	}

	protected void addOuterJoins(StringBuffer buffer) {
		buffer.append(getFilter().getJoins());
	}

	// ------------------------------------
	// lifecycle related
	// ------------------------------------

	@Create
	public void create() {
		addUpdateListener();
	}
	
	@Destroy
	@Remove
	@BypassInterceptors
	public void destroy() {
	}

	// ------------------------------------
	// paging related
	// ------------------------------------

	protected Pager pager = new Pager();
	
    @Out
    public Pager getPager() {
		return pager;
	}

	@Observer(value = Pager.EVENT, create = false)
    public void pageChanged() {
		updateResults();
    }

	// ------------------------------------
	// sorting related
	// ------------------------------------

	protected OrderBy orderBy = new OrderBy();

    @Out
	public OrderBy getOrderBy() {
		return orderBy;
	}

    @Observer(value = OrderBy.EVENT, create = false)
    public void orderChanged() {
		updateResults();
		pager.setPageNumber(0);
    }

	protected void initOrderBy(String name, boolean descending) {
		this.orderBy.initOrderBy(name, descending);
	}
	
	// ------------------------------------
	// nav related
	// ------------------------------------

	public String find() {
		// used in filter.xhtml
		updateResults();
		pager.setPageNumber(0);
		return null;
	}

	public String quickAdd() {
		// used in search-template.xhtml
		getEditor().quickAdd();
		return null;
	}

	public String delete() {
		// used in search-template.xhtml
		PersistenceProvider.instance().setFlushModeManual(getEntityManager());
		String id = Parameters.instance().getRequestParameters().get("id")[0];
		E entity = getEntityManager().find(getEntityClass(), convert(id));
		getEditor().delete(entity);
		return null;
	}

	protected Object convert(String id) {
		return new Long(id);
	}

	// ------------------------------------
	// render related
	// ------------------------------------
	
	@Out(value="renderPrint")
	public boolean isRenderPrint() {
		return true;
	}

	@Out(value="renderPrintDetails")
	public boolean isRenderPrintDetails() {
		return !isSimpleEntity();
	}

	@Out(value="renderExcel")
	public boolean isRenderExcel() {
		return true;
	}

	@Out(value="renderCharts")
	public boolean isRenderCharts() {
		return true;
	}

	@Out(value="renderCreate")
	public boolean isRenderCreate() {
		return !isSimpleEntity() && hasPermissions(Action.INSERT);
	}

	@Out(value="renderQuickAdd")
	public boolean isRenderQuickAdd() {
		return isSimpleEntity() && hasPermissions(Action.INSERT);
	}

	@Out(value="renderAction")
	public boolean isRenderAction() {
		return isRenderView() || isRenderEdit() || isRenderDelete()
				|| isRenderPrintDetails();
	}

	@Out(value="renderView")
	public boolean isRenderView() {
		return !isSimpleEntity();
	}

	@Out(value="renderEdit")
	public boolean isRenderEdit() {
		return hasPermissions(Action.UPDATE);
	}

	@Out(value="renderDelete")
	public boolean isRenderDelete() {
		return hasPermissions(Action.DELETE);
	}

	public boolean isSimpleEntity() {
		// TODO check annotations?
		return true;
	}

	@Out("finderViewPath")
	public String getViewPath() {
		return "";
	}

	public String getEditViewId() {
		return "/jsf/" + getViewPath() + getEntityName() + "/Edit.xhtml";
	}

	public String getViewViewId() {
		return "/jsf/" + getViewPath() + getEntityName() + "/View.xhtml";
	}
	
	protected String title;
	
	public String getTitle() {
		if (title == null) {
			title = ResourceBundle.instance().getString(
					getEntityName() + "_Menu");
		}
		return title;
	}

	// ------------------------------------
	// entity manager
	// ------------------------------------

	protected transient EntityManager entityManager;

	public EntityManager getEntityManager() {
		if (entityManager == null || !entityManager.isOpen()) {
			entityManager = (EntityManager) getComponentInstance( getPersistenceContextName() );
		}
		return entityManager;
	}
	
	protected String getPersistenceContextName() {
		return EntityPersistenceContextMap.instance()
				.getPersistenceContextNameFor(getEntityClass());
	}
	
	// ------------------------------------
	// type related
	// ------------------------------------

	private Class<E> entityClass;

	@SuppressWarnings("unchecked")
	public Class<E> getEntityClass() {
		if (entityClass == null) {
			entityClass = ReflectionUtil.getParameterizedType(getClass());
		}
		return entityClass;
	}

	@Out
	public String getEntityName() {
		return getEntityClass().getSimpleName();
	}

	@SuppressWarnings("unchecked")
	protected AbstractEditor<E> getEditor() {
		return (AbstractEditor<E>) Component.getInstance(getEditorName());
	}

	protected String getEditorName() {
		return StringUtils.uncapitalize(getEntityName()) + "Editor";
	}

	@Out("finderName")
	public String getName() {
		return Seam.getComponentName(getClass());
	}

	// ------------------------------------
	// utils
	// ------------------------------------

	protected String getCurrentUserName() {
		Identity identity = Identity.instance();
		if (identity.isLoggedIn()) {
			return identity.getCredentials().getUsername();
		} else {
			return null;
		}
	}

	protected boolean isUserInRole(String role) {
		return Identity.instance().hasRole(role);
	}

	protected boolean hasPermissions(Action action) {
		return PermissionsUtil.hasPermissions(getEntityClass(), action);
	}
	
	// ------------------------------------
	// Import/Export related
	// ------------------------------------

	@Out(value="renderImport")
	public boolean isRenderImport() {
		return true;
	}

	@Out(value="renderExport")
	public boolean isRenderExport() {
		return true;
	}

	public void getExportXml(OutputStream os) {
		BetwixtUtil.exportXml(getReportResult(), os,
				new EmptySuppressionStrategy());
	}
	
	@SuppressWarnings("unchecked")
	public void importXml(String xml) {
		Object o = BetwixtUtil.importXml(xml, getEntityClass());
		if (o instanceof Collection) {
			List<E> list = (List<E>) o;
				ImportUtil.doImport((List)list);
		} else {
			E e = (E) o;
			ImportUtil.doImport(e);
		}
		// can't validate the entire import unless we suspend validation during doImport and then validate
		// http://www.sleberknight.com/blog/sleberkn/entry/20071023
	}
	
	private byte[] importData;
	
	public void setImportData(byte[] importData) {
		this.importData = importData;
	}
	
	public byte[] getImportData() {
		return importData;
	}
	
	public String doImport() {
		if (importData == null) {
			getStatusMessages().addFromResourceBundleOrDefault(Severity.ERROR,
					"NoImportFile", "No import file was specified.");
		} else {
			importXml(new String(importData));
			importData = null;
			updateResults();
			getStatusMessages().addFromResourceBundleOrDefault(Severity.INFO,
					"ImportComplete", "Import Complete.");
		}
		return null;
	}

	// ------------------------------------
	// columns
	// ------------------------------------

	private List<ColumnType> columns;	
	
	@Out("columnTypes")
	public List<ColumnType> getColumns() {
		if (columns == null) {
			columns = new ArrayList<ColumnType>();
			initColumns();
		}
		return columns;
	}
	
	protected void initColumns() {
	}
	
	protected void addColumn(String name, String type, String label, String rendered) {
		ColumnType column = new ColumnType(name, ResourceBundle.instance().getString(label), type, rendered);
		columns.add(column);
	}

	protected void addColumn(String name, String type, String rendered) {
		addColumn(name, type, getEntityName() + "_" + name, rendered);
	}

	// ------------------------------------
	// polling
	// ------------------------------------
	
	public int getPollingInterval() {
		return 5000;
	}
	
	public boolean getPollingEnabled() {
		return false;
	}
}
